/*
 ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1

 * The contents of this file are subject to the Mozilla Public License Version 
 * 1.1 (the "License"); you may not use this file except in compliance with 
 * the License. You may obtain a copy of the License at 
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Blazing Games Inc. (Billy D. Spelchan) code.
 *
 * The Initial Developer of the Original Code is
 * Blazing Games Inc.
 * Portions created by the Initial Developer are Copyright (C) 1999, 2003, 2005-2006
 * the Initial Developer. All Rights Reserved.
 * 
 * Contributor(s):
 *
 ***** END LICENSE BLOCK *****
 */
package com.blazinggames.holeman;

import java.awt.*;
//import java.awt.image.*;
import java.awt.event.*;
import java.io.*;
import java.net.*;

public class GameScreen
extends Panel
implements Runnable, KeyListener {
// *** CONSTANTS ***
  private final static int ACTION_NEXT = 1;
  private final static int ACTION_KILLED = 2;
  private final static int ACTION_GAMEOVER = 3;
  private final static int ACTION_RESET = 4;

  public static final int MODE_PLAY = 0;
  public static final int MODE_TEST = 1;
  public static final int TILE_PLAY[] =
        {0, 5, 6, 7, 4, 5, 6, 7, 8, 9, 10, 11, 12, 0, 0, 0};
  public static final int TILE_TEST[] =
        {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};

  final static Font fontBIGINFO = new Font("TimesRoman", Font.BOLD, 24);
  final static Font fontSMALLINFO = new Font("TimesRoman", Font.BOLD, 12);

// *** VARIABLES ***
	ActionListener eventActionListener;
  int nAction;
  boolean bWaitingForAction;
  Map map;

  // Render screen information - used to draw tiles in various resolutions
  int nScreenWidth, nScreenHeight;
  int nTileWidth, nTileHeight;
  int nPlayMode;

  // Data for the display
  Image iTiles;
  Image iSprites;

  // Data for the backbuffer
  Image iBuffer;
  Graphics gBuffer;

  Thread runner;

  boolean bFullPaint, bValidPaint;

  int nOldNumCrystals, nOldNumLives, nOldNumHoles;
  boolean bOldSound;

  URL _codeBase;
  
// *** CONSTRUCTORS ***
  public GameScreen(Map m, ActionListener al, Class a, URL codebase, boolean bJar) {
      _codeBase = codebase;
    eventActionListener = al;
//    screenLow(a, bJar);
    screenHigh(bJar);
    map = m;
    map.restartLevel(0);
    runner = new Thread(this);
    runner.start();
    addKeyListener(this);
    requestFocus();
    bWaitingForAction = false;
  }

  void screenHigh(boolean bJar)
  {
    Toolkit tk = Toolkit.getDefaultToolkit();

    if (bJar) {
      InputStream instreamImage = tk.getClass().getResourceAsStream("res/holeman/tiles.gif");
      if (instreamImage == null) {
        System.err.println("Can not get image from JAR!-tiles");
        return;
      }
      try {
        byte imageBytes[] = new byte[instreamImage.available()];
        instreamImage.read(imageBytes);
        instreamImage.close();
        iTiles = tk.createImage(imageBytes);
      } catch (IOException e) {
        System.err.println("Can not get image from JAR!-tiles");
      }

      instreamImage = tk.getClass().getResourceAsStream("res/holeman/sprites.gif");
      if (instreamImage == null) {
        System.err.println("Can not get image from JAR!-sprites");
        return;
      }
      try {
        byte imageBytes[] = new byte[instreamImage.available()];
        instreamImage.read(imageBytes);
        instreamImage.close();
        iTiles = tk.createImage(imageBytes);
      } catch (IOException e) {
        System.err.println("Can not get image from JAR!-sprites");
      }
    }
    else {
        if (_codeBase != null)
        {
            try
            {            
                URL temp = new URL(_codeBase, "res/holeman/tiles.gif");
                iTiles = tk.getImage(temp);
                temp = new URL(_codeBase, "res/holeman/sprite.gif");
                iSprites = tk.getImage(temp);
            }
            catch (MalformedURLException mue)
            {
                System.err.println("Problem loading tiles/sprites: " + mue.getMessage());
            }
        }
        else
        {
            System.out.println("loading via toolkit");
            iTiles = tk.getImage("res/holeman/tiles.gif");
            iSprites = tk.getImage("res/holeman/sprite.gif");            
        }
    }

    MediaTracker tracker  =  new  MediaTracker(this);
    tracker.addImage(iTiles, 1);
    tracker.addImage(iSprites, 1);
    try  {
      tracker.waitForID(1);
    }  catch  (InterruptedException  e)  {
      System.err.println("Image loading Interrupted Exception");
    }
    if (tracker.isErrorID(1))
      System.err.println("Image loading Error - can not load tiles");

    nScreenWidth = 640;
    nScreenHeight = 440;
    nTileWidth = nTileHeight = 32;

    iBuffer = null;
    gBuffer = null;

    System.gc();
    bFullPaint = true;
  }

  void screenLow(Class a, boolean bJar) {
    Toolkit tk = Toolkit.getDefaultToolkit();
    if (bJar) {
      InputStream instreamImage = a.getResourceAsStream("stiles.jpg");
      if (instreamImage == null) {
        System.err.println("1 Can not get image from JAR!-tiles");
        return;
      }
      try {
        byte imageBytes[] = new byte[instreamImage.available()];
        instreamImage.read(imageBytes);
        instreamImage.close();
        iTiles = tk.createImage(imageBytes);
      } catch (IOException e) {
        System.err.println("2 Can not get image from JAR!-tiles");
      }

      instreamImage = a.getResourceAsStream("ssprite.gif");
      if (instreamImage == null) {
        System.err.println("Can not get image from JAR!-sprites");
        return;
      }
      try {
        byte imageBytes[] = new byte[instreamImage.available()];
        instreamImage.read(imageBytes);
        instreamImage.close();
        iSprites = tk.createImage(imageBytes);
      } catch (IOException e) {
        System.err.println("Can not get image from JAR!-sprites");
      }
    }
    else {
      iTiles = tk.getImage("res/holeman/stiles.jpg");
      iSprites = tk.getImage("res/holeman/ssprite.gif");
    }

    MediaTracker tracker  =  new  MediaTracker(this);
    tracker.addImage(iTiles, 1);
    tracker.addImage(iSprites, 1);
    try  {
      tracker.waitForID(1);
    }  catch  (InterruptedException  e)  {
      System.err.println("Image loading Interrupted Exception");
    }
    if (tracker.isErrorID(1))
      System.err.println("Image loading Error");

    nScreenWidth = 320;
    nScreenHeight = 220;
    nTileWidth = nTileHeight = 16;

    iBuffer = null;
    gBuffer = null;

    System.gc();
    bFullPaint = true;
  }

  public void setTest(boolean b) {
    if (b)
      nPlayMode = MODE_TEST;
    else
      nPlayMode = MODE_PLAY;
  }

/*
	public void start() {
    System.out.println("Thread start");
		if (runner == null) {
			runner = new Thread(this);
			runner.start();
		}
	}
*/
	public void run() {
    long curTime, secTime, lastTime;
    int nRate = 0, nFrames = 1;

    curTime = secTime = System.currentTimeMillis() + 1000;
		while (true) {
      while (!isShowing()) {
        try {
          Thread.sleep(100,0);
        }
        catch(InterruptedException e) {  }
      }

      // Adjust for the current frame
      lastTime = curTime;
      adjust(nFrames);
      ++nRate;

      // Force the frame to be drawn
      synchronized(this) {
        repaint();
	  		try {
          wait();
		  	}
        catch(InterruptedException e) {  }
      } // End synch

      // Make sure at least 1/32 seconds have passed
      curTime = System.currentTimeMillis();
      if ((lastTime+30) > curTime) {
        try {
          Thread.sleep(30,0);
        }
        catch(InterruptedException e) {  }
        curTime = System.currentTimeMillis();
      }

      nFrames = (int)((curTime - lastTime) / 30);
      if (nFrames > 16) nFrames = 16;
      if (curTime > secTime) {
//        requestFocus();
//        System.out.println("Frame Rate: " + nRate +
//        " X:" + map.player.getX() + " Y:" + map.player.getY());
        nRate = 0;
        secTime = curTime + 1000;
      }
		} // End while
	}
/*
	public void stop() {
    System.out.println("Thread stop");
		if (runner != null) {
			runner.stop();
			runner = null;
		}
	}
*/

/*
  public void destroy() {
		if (runner != null) {
			runner.stop();
			runner = null;
		}
    super.destroy();
  }
*/

  public synchronized void paint(Graphics g) {
    if (iBuffer != null)
      g.drawImage(iBuffer, 0, 0, this);
		notifyAll();
  }

  public void update(Graphics g) {
    bValidPaint = true;
    paint(g);
  }

  private void refreshTiles(int x, int y) {
    int tileTrans[], temp;

    int nX = x / 32;
    int nY = y / 32;

    if (nPlayMode == MODE_PLAY)
      tileTrans = TILE_PLAY;
    else
      tileTrans = TILE_TEST;

    temp = tileTrans[map.getTile(nX, nY)];
    gBuffer.drawImage(iTiles, nX * nTileWidth, nY * nTileHeight,
          nX * nTileWidth + nTileWidth, nY * nTileHeight + nTileHeight,
          temp * nTileWidth, 0, temp*nTileWidth+nTileWidth, nTileHeight,
          this);
    if ((x % nTileWidth) != 0) {
      ++nX;
      temp = tileTrans[map.getTile(nX, nY)];
      gBuffer.drawImage(iTiles, nX * nTileWidth, nY * nTileHeight,
            nX * nTileWidth + nTileWidth, nY * nTileHeight + nTileHeight,
            temp * nTileWidth, 0, temp*nTileWidth+nTileWidth, nTileHeight,
            this);
    }
    if ((y % nTileHeight) != 0) {
      ++nY;
      temp = tileTrans[map.getTile(nX, nY)];
      gBuffer.drawImage(iTiles, nX * nTileWidth, nY * nTileHeight,
            nX * nTileWidth + nTileWidth, nY * nTileHeight + nTileHeight,
            temp * nTileWidth, 0, temp*nTileWidth+nTileWidth, nTileHeight,
            this);
      if ((x % nTileWidth) != 0) {
        --nX; // Was increased above so is off one!
        temp = tileTrans[map.getTile(nX, nY)];
        gBuffer.drawImage(iTiles, nX * nTileWidth, nY * nTileHeight,
              nX * nTileWidth + nTileWidth, nY * nTileHeight + nTileHeight,
              temp * nTileWidth, 0, temp*nTileWidth+nTileWidth, nTileHeight,
              this);
      }
    }

  }

  private void paintSprite(int x, int y, int id) {
    int spRow = (id / 16) * nTileHeight;
    int spCol = (id % 16) * nTileWidth;

    int nx = x * nTileWidth / 32;
    int ny = y * nTileHeight / 32;

    gBuffer.drawImage(iSprites, nx, ny, nx+nTileWidth, ny+nTileHeight,
          spCol, spRow, spCol+nTileWidth, spRow+nTileHeight, this);
  }

  private void paintStatusBar(boolean bForceRedraw) {
    if ((bForceRedraw) ||
          (nOldNumCrystals != map.player.getCrystals()) ||
          (nOldNumLives != map.player.getLives()) ||
          (nOldNumHoles != map.player.getHoles()) ||
          (bOldSound != map.bSoundEnabled ) ){
      int nTop = 12*nTileHeight;
      int nGap = nTileHeight / 4;
      int nSpriteTop = nTop + nGap + 2;
      int nTextLine = nTop +nTileHeight;
      int nSpriteScale = 1;

      // update numbers
      nOldNumCrystals = map.player.getCrystals();
      nOldNumLives = map.player.getLives();
      nOldNumHoles = map.player.getHoles();
      bOldSound = map.bSoundEnabled;

      // clear status bar and prepare font
      gBuffer.setColor(Color.gray);
      gBuffer.fillRect(0, nTop, 20*nTileWidth, 2*nTileHeight);
      gBuffer.setColor(Color.darkGray);
      if (nTileWidth > 16)
        gBuffer.setFont(fontBIGINFO);
      else {
        nSpriteTop *= 2;
        nSpriteScale = 2;
        gBuffer.setFont(fontSMALLINFO);
      }

      // draw number of lives
      gBuffer.draw3DRect(nGap, nTop+nGap, nTileWidth*4-nGap-nGap, nTileHeight+2, true);
      paintSprite((nGap+2)*nSpriteScale, nSpriteTop, Sprite.FRM_PLAYER);
      gBuffer.drawString(nOldNumLives+"", nGap+4+nTileWidth, nTextLine);

      // Draw number of crystals
      gBuffer.draw3DRect(nGap+nTileWidth*4, nTop+nGap, nTileWidth*7-nGap-nGap, nTileHeight+2, true);
      paintSprite((nGap+nTileWidth*4+2)*nSpriteScale, nSpriteTop, Sprite.FRM_CRYSTAL);
      gBuffer.drawString(nOldNumCrystals + " of "+map.getNumCrystals(), nGap+nTileWidth*5+4, nTextLine);

      // Draw number of holes
      gBuffer.draw3DRect(nGap+nTileWidth*11, nTop+nGap, nTileWidth*7-nGap-nGap, nTileHeight+2, true);
      paintSprite((nGap+nTileWidth*11+2)*nSpriteScale, nSpriteTop, Sprite.FRM_PORTHOLE);
      gBuffer.drawString(nOldNumHoles + " of "+map.getNumHoles(), nGap+nTileWidth*12+4, nTextLine);

      // Paint Sound Status
      if (map.bSoundEnabled)
        paintSprite((nGap+nTileWidth*18+2)*nSpriteScale, nSpriteTop, Sprite.FRM_SOUND);
      else
        paintSprite((nGap+nTileWidth*18+2)*nSpriteScale, nSpriteTop, Sprite.FRM_NOSOUND);
    }
  }


  private synchronized void render() {
    int tileTrans[], temp;
    int cntr;
    Monster m;
    Hole h;

    if (iBuffer == null) {
      iBuffer = createImage(nScreenWidth, nScreenHeight);
      if (iBuffer == null) {
//        System.out.println("Error creating buffer!");
        return;
      }
      gBuffer = iBuffer.getGraphics();
    }

    if (nPlayMode == MODE_PLAY)
      tileTrans = TILE_PLAY;
    else
      tileTrans = TILE_TEST;

    if ( (map.isFullRenderNeeded()) || (bFullPaint)) {
      bFullPaint = false;
      map.setFullRender(false);
      for (int cntrY = 0; cntrY < 12; ++cntrY)
        for(int cntrX = 0; cntrX < 20; ++cntrX) {
          temp = tileTrans[map.getTile(cntrX, cntrY)];
          gBuffer.drawImage(iTiles, cntrX * nTileWidth, cntrY * nTileHeight,
                cntrX * nTileWidth + nTileWidth, cntrY * nTileHeight + nTileHeight,
                temp * nTileWidth, 0, temp*nTileWidth+nTileWidth, nTileHeight,
                this);
        }
        paintStatusBar(true);
    }

    // Clear potential player garbage
    synchronized(map.player) {
      refreshTiles(map.player.getLastX(), map.player.getLastY());
    }
   // And hole garbage
   synchronized(map.vHoles) {
     for (int cntrH = 0; cntrH < map.vHoles.size(); ++cntrH) {
        h = (Hole)(map.vHoles.elementAt(cntrH));
        refreshTiles(h.getX(), h.getY());
     }
   }
   // And monster garbage
   synchronized(map.vMonsters) {
     for (int cntrM = 0; cntrM < map.vMonsters.size(); ++cntrM) {
        m = (Monster)(map.vMonsters.elementAt(cntrM));
        refreshTiles(m.getLastX(), m.getLastY());
     }
   }

   paintStatusBar(false);

    // Paint crystals
    Sprite sp;

    for (cntr = 0; cntr < map.vCrystals.size(); ++cntr) {
      sp = (Sprite)(map.vCrystals.elementAt(cntr));
      if (sp.isVisible())
          paintSprite(sp.getX(), sp.getY(), sp.getImageID());
    }

     // And holes
     for (int cntrH = 0; cntrH < map.vHoles.size(); ++cntrH) {
        h = (Hole)(map.vHoles.elementAt(cntrH));
        if (h.isVisible())
          paintSprite(h.getX(), h.getY(), h.getImageID());
     }

    // Paint monsters
    synchronized (map.vMonsters) {
      for (cntr = 0; cntr < map.vMonsters.size(); ++cntr) {
        m = (Monster)(map.vMonsters.elementAt(cntr));
        if (m.isVisible())
          paintSprite(m.getX(), m.getY(), m.getImageID());
        m.setLastLocation();
      }
    }

    if (map.levelExit.isVisible())
      paintSprite(map.levelExit.getX(), map.levelExit.getY(), Sprite.FRM_EXIT);

    synchronized(map.player) {
      paintSprite(map.player.getX(), map.player.getY(), map.player.getImageID());
      map.player.setLastLocation();
    }
  }

  private void handleAction() {
    String sAction;

    if (map.player.nCommand == 0) {
      bFullPaint = true;
      bWaitingForAction = false;
      switch (nAction) {
        case ACTION_NEXT :
          map.nextLevel();
          sAction = "Next";
          break;
        case ACTION_KILLED :
          map.restartLevel(map.nCurLevel);
          sAction = "Killed";
          break;
        case ACTION_GAMEOVER :
          sAction = "GameOver";
          break;
        case ACTION_RESET :
          map.restartLevel(map.nCurLevel);
          sAction = "Reset";
          break;
        default :
          sAction = "X";
      }
      eventActionListener.actionPerformed(
            new ActionEvent(this, ActionEvent.ACTION_PERFORMED, sAction));
    }
    else
      return;
  }

  public void adjust(int nTurns) {
//    int cntrX, cntrY;

    if (bWaitingForAction) {
      handleAction();
      return;
    }

    int r = map.doUpdate(nTurns);
    if (r == Map.LEVEL_EXITED) {
      nAction = ACTION_NEXT;
      bWaitingForAction = true;
    }
    else if (r == Map.LEVEL_KILLED) {
      if (map.player.getLives() > 0) {
        nAction = ACTION_KILLED;
        bWaitingForAction = true;
      }
      else {
        nAction = ACTION_GAMEOVER;
        bWaitingForAction = true;
      }
    }
    else if (r == Map.LEVEL_RESET) {
      if (map.player.getLives() > 0) {
        nAction = ACTION_RESET;
        bWaitingForAction = true;
      }
      else {
        nAction = ACTION_GAMEOVER;
        bWaitingForAction = true;
      }
    }

    if (!bWaitingForAction)
      render();
  }

  /**
   * Invoked when a key has been typed.
   * This event occurs when a key press is followed by a key release.
   */
  public void keyTyped(KeyEvent e) { /* IGNORE */ }


  /**
  */
  private int getCommandID(int nKey) {
    int nCommand = 0;

    switch (nKey) {
      case KeyEvent.VK_LEFT:  // Move left
        nCommand = Player.CMD_LEFT;
        break;
      case KeyEvent.VK_RIGHT: // Move right
        nCommand = Player.CMD_RIGHT;
        break;
      case KeyEvent.VK_UP:  // Climb
        nCommand = Player.CMD_CLIMB_UP;
        break;
      case KeyEvent.VK_DOWN:  // Climb
        nCommand = Player.CMD_CLIMB_DOWN;
        break;
      case KeyEvent.VK_COMMA:
      case KeyEvent.VK_Z:
        nCommand = Player.CMD_HOLE_LEFT;
        break;
      case KeyEvent.VK_PERIOD:
      case KeyEvent.VK_X:
        nCommand = Player.CMD_HOLE_RIGHT;
        break;
      case KeyEvent.VK_Q:
        nCommand = Player.CMD_RESTART;
        break;
      default:
        nCommand = 0;
    } // end switch

//      System.out.println("Key: " + nKey + " Result:" + nCommand);

    return nCommand;
  }

  /**
   * Invoked when a key has been pressed.
   */
  public void keyPressed(KeyEvent e) {
    map.player.command(getCommandID(e.getKeyCode()), true);
  }

  /**
   * Invoked when a key has been released.
   */
  public void keyReleased(KeyEvent e) {
    if (e.getKeyCode() == KeyEvent.VK_T)
      map.setSound(!map.bSoundEnabled);

    map.player.command(getCommandID(e.getKeyCode()), false);
  }

}